/*
MIT License

Copyright (c) 2021 МГТУ им. Н.Э. Баумана, кафедра ИУ-6, Михаил Фетисов,

https://bmstu.codes/lsx/simodo
*/

#include "simodo/interpret/Interpret.h"
#include "simodo/interpret/AnalyzeException.h"

#include "simodo/loom/Loom_interface.h"
#include "simodo/bormental/DrBormental.h"
#include "simodo/inout/convert/functions.h"
#include "simodo/inout/format/fmt.h"

#include <algorithm>
#include <cassert>

#define SemanticFiber_DEBUG_no

namespace 
{
    using namespace simodo;

    void moveValue(variable::Variable & to, const variable::Variable & from)
    {
        if (to.type() == variable::ValueType::Ref) {
            variable::VariableRef ref = to.value().getRef();
            ref.setValue(from.origin().value());
        }
        else
            to.value() = from.origin().value();
    }

    void copyValue(variable::Variable & to, const variable::Variable & from)
    {
        if (to.type() == variable::ValueType::Ref) {
            variable::VariableRef ref = to.value().getRef();
            ref.setValue(from.origin().value().copy());
        }
        else
            to.value() = from.origin().value().copy();
    }

}

namespace simodo::interpret
{
    void Interpret::assignVariable(variable::Variable & target, const variable::Variable & source, bool need_copy) 
    {
        // Правила присвоения и перемещения:
        // 1. Если в качестве источника (правой части присваивания) выступает переменная с признаком ошибки, 
        // то распространяем ошибку на целевую переменную.
        // 2. Если в качестве целевой переменной выступает null, значит она не задана можно тупо присвоить.
        // 3. Если в качестве источника выступает ссылка, это значит это ссылка на уже объявленную
        // переменную и мы должны скопировать.
        // 4. Если лежит не ссылка, то это значит источником является временная переменная, которую
        // можно частично перенести.
        // 5. Если источником является ссылка с пометкой SPEC_REFERENCE, значит нужно целевую
        // переменную сделать ссылкой на источник.

        variable::Variable &       target_origin = target.origin();
        const variable::Variable & source_origin = source.origin();

        // 1.
        if (source_origin.value().isError()) {
            target_origin.value() = source_origin.value();
            return;
        }

        // 2.
        if (target_origin.type() == variable::ValueType::Null) {
            if (source_origin.type() != variable::ValueType::Null)
                target_origin.value() = source_origin.value();
            return;
        }

        // 3.
        if (source.value().isRef())
            need_copy = true;

        // 4. (учитывается за счёт задания need_copy = false в параметрах функции по умолчанию)

        // 5.
        if (source.value().isRef()) //-V581
        {
            const variable::Value & ref_value = source.spec().find(variable::SPEC_REFERENCE);
            if (ref_value.isBool() && ref_value.getBool()) {
                target_origin.value() = source_origin.value();
                return;
            }
        }

        switch(target_origin.type())
        {
        case variable::ValueType::Array:
            if (need_copy)
                copyValue(target, source_origin);
            else
                moveValue(target, source_origin);
            break;
        case variable::ValueType::Object:
            assignObject(target_origin, source_origin, need_copy);
            break;
        case variable::ValueType::Function:
            target_origin.value() = source_origin.value();
            break;
        default:
            if (need_copy)
                copyValue(target, convertVariable(source_origin,target_origin.type(), need_copy));
            else
                moveValue(target, convertVariable(source_origin,target_origin.type(), need_copy));
            break;
        }
    }

    void Interpret::assignArray(variable::Variable & /*target_origin*/, const variable::Variable & /*source_origin*/,
                                bool /*need_copy*/) 
    {
        throw bormental::DrBormental("OperationEngine::assignArray", inout::fmt("Unsupported"));
    }

    void Interpret::assignObject(variable::Variable & target_origin, const variable::Variable & source_origin,
                                bool need_copy)
    {
        assert(target_origin.type() != variable::ValueType::Ref);
        assert(source_origin.type() != variable::ValueType::Ref);

        if (source_origin.type() != variable::ValueType::Object) {
            assignObject(target_origin, convertVariable(source_origin,variable::ValueType::Object), need_copy);
            return;
        }

        if (target_origin.type() == variable::ValueType::Null) {
            target_origin.value() = source_origin.value();
            return;
        }

        if (target_origin.type() != variable::ValueType::Object)
            throw bormental::DrBormental("assignObject", 
                            inout::fmt("Incorrect internal type of variable '%1'")
                            .arg(target_origin.name()));

        // С этого момента левая и правая части являются объектами

        const std::shared_ptr<variable::Object> rvalue_record = source_origin.value().getObject();
        std::shared_ptr<variable::Object>       lvalue_record = target_origin.value().getObject();

        for(const variable::Variable & rv : rvalue_record->variables()) {
            variable::Variable & lv = lvalue_record->getVariableByName_mutable(rv.name());
            if (!lv.name().empty()) {
                assignVariable(lv.origin(), rv.origin(), need_copy);
                continue;
            }

            /// \todo Нужно решить как лучше быть:
            /// 1. Добавлять недостающие элементы (как в JS/TS)
            /// 2. Заставлять пользователя самому добавлять не нужные ему элементы (иначе - ошибка), а потом затирать
            /// 3. Игнорировать ситуацию

            // Пробуем найти сеттер
            const variable::Variable & setter = lvalue_record->getVariableByName(u"set_" + rv.name());

            if (!setter.name().empty() && setter.origin().type() == variable::ValueType::Function)
                throw AnalyzeException( "OperationEngine::assignObject",
                                        rv.location().makeLocation(files()),
                                        inout::fmt("Using a setter '%1' when assigning an object is not supported")
                                        .arg(rv.name()));

            throw AnalyzeException( "OperationEngine::assignObject",
                                    rv.location().makeLocation(files()),
                                    inout::fmt("Element '%1' was not found in the target structure")
                                    .arg(rv.name()));
        }
    }

    variable::Variable Interpret::convertVariable(const variable::Variable & var, variable::ValueType type,
                                                bool need_copy) const
    {
        if (var.origin().type() == type) {
            /// \todo Нужно оптимизировать!?
            if (need_copy)
                return var.copyVariable();

            return var;
        }

        switch(type)
        {
        case variable::ValueType::String:
            if (var.origin().variant().index() == std::variant_npos)
                return { var.origin().name(), type, var.location(), var.spec() };
            return { {}, toString(var.origin().value()), var.location(), {} };
        case variable::ValueType::Float:
            if (var.origin().type() == variable::ValueType::Int) {
                if (var.origin().variant().index() == std::variant_npos)
                    return { var.origin().name(), type, var.location(), var.spec() };
                else
                    return { {}, 
                            static_cast<double>(std::get<int64_t>(var.origin().value().variant())), 
                            var.location(), 
                            {} };
            }
            break;
        case variable::ValueType::Array:
            // return { {}, 
            //         std::make_shared<variable::Array>(var.type(), std::vector<variable::index_t> {1}, std::vector<variable::Value> {var.origin().value()}), 
            //         var.location(), 
            //         {}};
            break;
        case variable::ValueType::Function:
            // if (var.origin().type() == variable::ValueType::Object) {
            //     std::shared_ptr<variable::Object> object    = var.origin().value().getObject();
            //     const variable::Variable          init_func = object->getVariableByName(u"__init__");
            //     if (!init_func.name().empty()) {
            //         if (init_func.type() == variable::ValueType::Function) {
            //             std::shared_ptr<variable::Object> function_object  = init_func.value().getObject();
            //             if (function_object->variables().size() > 1
            //              && function_object->variables()[0].type() == variable::ValueType::IntFunction)
            //                 function_object->variables()[0].value().getIntFunctionParent() = object;
            //         }
            //         return init_func;
            //     }
            // }
            break;
        default:
            break;
        }

        throw AnalyzeException("Interpret::convertVariable",
                                var.location().makeLocation(files()),
                                inout::fmt("Invalid type conversion from %1 to %2")
                                .arg(getValueTypeName(var.origin().type()))
                                .arg(getValueTypeName(type)));
    }

    std::shared_ptr<variable::Object> Interpret::self()
    {
        boundary_index_t function_bound = stack().findNearestMarkedBoundary(BOUNDARY_MARK_FUNCTION_FRAME);
        if (function_bound == UNDEFINED_BOUNDARY_INDEX)
            return {};

        const auto           [begin,end]       = stack().boundaryLowAndTop(function_bound);
        name_index_t         function_index    = begin-1;
        variable::Variable & function_variable = stack().variable(function_index).origin();

        assert(function_variable.type() == variable::ValueType::Function);
        
        std::shared_ptr<variable::Object> function_object = function_variable.value().getFunction();

        assert(!function_object->variables().empty()
             && function_object->variables()[0].type() == variable::ValueType::IntFunction);

        return function_object->variables()[0].value().getIntFunctionParent();
    }

    std::shared_ptr<variable::Object> Interpret::makeSelf()
    {
        boundary_index_t module_bound = stack().findNearestMarkedBoundary(BOUNDARY_MARK_MODULE_FRAME, BoundScope::Global);
        if (module_bound == UNDEFINED_BOUNDARY_INDEX)
            return {};

        auto [begin_index, end_index] = stack().boundaryLowAndTop(module_bound);
        variable::VariableSet_t vars;

        for(name_index_t i = begin_index; i < end_index; ++i) {
            variable::Variable & v = stack().variable(i);
            vars.push_back(v);
        }

        return std::make_shared<variable::Object>(vars);
    }

    void Interpret::callPreparedFunction(
                            Interpret_interface * p_fiber,
                            boundary_index_t boundary_index, 
                            Notification_interface & notify,
                            bool invoke)
    {
        assert(p_fiber);

        if(p_fiber != this) {
            auto                        [begin, end]    { stack().boundaryLowAndTop(boundary_index) };
            name_index_t                function_index  { begin - 1 };
            const variable::Variable &  function        { stack().variable(function_index) };

            name_index_t another_function_index = p_fiber->stack().find(function.origin().name());

            assert(another_function_index != UNDEFINED_NAME_INDEX);

            p_fiber->stack().push({u"", p_fiber->stack().variable(another_function_index).makeReference(), function.location()});
            boundary_index_t another_boundary_index = p_fiber->stack().startBoundary();
            for(name_index_t i=begin; i < end; ++i) {
                variable::Variable & v = stack().variable(i);
                if (v.origin().value().isFunction())
                    p_fiber->stack().push(v.origin());
                else
                    p_fiber->stack().push(v.copyVariable());
            }

            stack().clearBoundary(boundary_index);
            stack().pop();

            boundary_index = another_boundary_index;
        }

        auto                        [begin, end]    { p_fiber->stack().boundaryLowAndTop(boundary_index) };
        name_index_t                function_index  { begin - 1 };
        const variable::Variable &  function        { p_fiber->stack().variable(function_index) };

        variable::FunctionWrapper   func                 = function.origin();
        variable::ValueType         declared_return_type = func.getReturnDeclarationVariable().type();
        inout::TokenLocation        function_location    = func.function_variable().location();

        p_fiber->expr().callPreparedFunction(
                boundary_index,
                notify,
                invoke,
                [p_fiber, boundary_index, declared_return_type, function_location](const FlowLayerInfo & flow) 
                {
                    p_fiber->stack().clearBoundary(boundary_index);
                    p_fiber->stack().pop();
                    p_fiber->stack().push({u"", p_fiber->expr().return_value(), flow.code.token().location()});

                    if (flow.error_sign) 
                        return;

                    if (declared_return_type != variable::ValueType::Null
                     && p_fiber->expr().return_value().isNull() && !p_fiber->expr().return_value().isError())
                        throw AnalyzeException("Interpret::callPreparedFunction", 
                                                function_location.makeLocation(p_fiber->files()), 
                                                inout::fmt("The function must return a value"));
                });
    }

    void Interpret::callPreparedFunction(
                            boundary_index_t boundary_index, 
                            Notification_interface & notify,
                            bool invoke,
                            const std::function<void(const FlowLayerInfo &)> callback)
    try
    {
        auto                                 [begin, end]    { stack().boundaryLowAndTop(boundary_index) };
        name_index_t                         function_index  { begin - 1 };
        const variable::Variable &           function        { stack().variable(function_index) };
        const variable::Variable &           function_origin { function.origin() };
        variable::VariableSetWrapper_mutable args            { stack().makeVariableSetWrapper() };
        variable::FunctionWrapper            func            { function_origin };
        variable::VariableSetWrapper         decl            { func.getArgumentDeclarationVariables() };

        if (args.size() != decl.size())
            throw AnalyzeException("Interpret::callPreparedFunction", 
                                    function.location().makeLocation(files()),
                                    inout::fmt("The function call '%1' contains an incorrect number of arguments")
                                    .arg(function.name()));

        for(size_t i=0; i < decl.size(); ++i) {
            if (args[i].origin().type() != decl[i].type() && decl[i].type() != variable::ValueType::Null)
                args[i] = convertVariable(args[i], decl[i].type());

            args[i].setName(decl[i].name());
        }

        if (func.getCallingAddressVariable().type() == variable::ValueType::ExtFunction) {
            variable::Value return_value;

            if (invoke)
                return_value = func.invoke(args);
            else
                return_value = func.getReturnDeclarationVariable().value();

            stack().clearBoundary(boundary_index);
            stack().pop();
            stack().push({u"", return_value, function.location()}); //, {{{variable::SPEC_REFERENCE, true}}}});
            return;
        }

        assert(func.getCallingAddressVariable().type() == variable::ValueType::IntFunction);

        _return_value = {variable::ValueType::Null};

        if (invoke) {
            const variable::InternalFunction & internal_function 
                = std::get<variable::InternalFunction>(func.getCallingAddressVariable().value().variant());

            const variable::Value & tethered_value = function_origin.spec().object()->find(variable::SPEC_TETHERED);

            if (tethered_value.isBool() && tethered_value.getBool()) {
                boundary_index_t module_bound_index = stack().findNearestMarkedBoundary(BOUNDARY_MARK_MODULE_FRAME, BoundScope::Global);
                assert(module_bound_index != UNDEFINED_BOUNDARY_INDEX);  
                const auto [begin,end] = stack().boundaryLowAndTop(module_bound_index);              
                for(name_index_t i = begin; i < end; ++i)
                    stack().pushRef(stack().variable(i), function.location());
            }
            else
                for(const variable::Variable & closure : internal_function.closures.variables()) 
                    if (closure.name() == SELF_VARIABLE_NAME) {
                        std::shared_ptr<variable::Object> self_object = self();

                        if (!self_object)
                            self_object = makeSelf();
                            
                        variable::Variable self = {{}, self_object, closure.location()};
                        stack().push(self);
                    }
                    else {
                        if (closure.name() == function_origin.name())
                            stack().push(function_origin);
                        else 
                            stack().push(closure);
                            
                        notify.notifyDeclared(closure);
                    }

            stack().setBoundaryToGlobal(boundary_index);

            bool need_to_stretch = _layer_stack.empty();

            addFlowLayer( *internal_function.code, boundary_index, callback);

            notify.notifyBeforeFunctionCalling(function_origin);

            if (need_to_stretch)
                _loom.stretch(this);
        }
        else {
            stack().clearBoundary(boundary_index);
            stack().pop();
            variable::Variable return_variable = func.getReturnDeclarationVariable();
            stack().push(return_variable);
        }
    }
    catch(...) {
        // Перед выходом восстанавливаем ожидаемое состояние выполнения команды
        stack().clearBoundary(boundary_index);
        stack().pop();
        stack().push(variable::error_variable());
        throw;
    }

    void Interpret::print(bool need_detailed_report) const
    {
        const variable::Variable var = _stack.variable(_stack.top()).origin();
        std::u16string           str = toString(var.value(), need_detailed_report);

        if (need_detailed_report && !var.spec().object()->variables().empty())
            str += u"#" + toString(var.spec().object(), need_detailed_report);

        reporter().reportInformation(inout::toU8(str));
    }

    std::u16string Interpret::toString(const variable::Value & value, bool need_whole_info, bool quote_strings) const
    {
        std::u16string str;

        switch(value.type())
        {
        case variable::ValueType::String:
            if (!std::holds_alternative<std::u16string>(value.variant()))
                str = u"<"+inout::toU16(variable::getValueTypeName(value.type()))+ u">";
            else {
                std::u16string text = std::get<std::u16string>(value.variant());

                if (quote_strings) {
                    inout::replaceAll(text, u"\\", u"\\\\");
                    inout::replaceAll(text, u"\"", u"\\\"");
                    text = u"\""+text+u"\"";
                }
                str = text;
            }
            break;
        case variable::ValueType::Bool:
            str = std::holds_alternative<bool>(value.variant())
                    ? (std::get<bool>(value.variant()) ? u"true" : u"false")
                    : u"<"+inout::toU16(variable::getValueTypeName(value.type()))+ u">";
            break;
        case variable::ValueType::Int:
            str = std::holds_alternative<int64_t>(value.variant())
                    ? inout::toU16(std::to_string(std::get<int64_t>(value.variant())))
                    : u"<"+inout::toU16(variable::getValueTypeName(value.type()))+ u">";
            break;
        case variable::ValueType::Float:
            str = std::holds_alternative<double>(value.variant())
                    ? inout::toU16(inout::clearNumberFractionalPart(std::to_string(std::get<double>(value.variant()))))
                    : u"<"+inout::toU16(variable::getValueTypeName(value.type()))+ u">";
            break;
        // case ValueType::Undefined:
        //     str = u"undef";
        //     break;
        // case ValueType::Function:
        //     if (name.substr(0,4) == u"get_") {
        //         const VariableSet_t & params = get<shared_ptr<Record>>(value.variant())->variables();
        //         assert(params.size() >= 2);
        //         if(params.size() == 2 && params[1].declared != DeclaredType::Error) {
        //             callFunction(var,{},InterpreterMode::FullExecution);
        //             return toString(_return_value, need_whole_info, true);
        //         }
        //     }
        //     [[fallthrough]];
        case variable::ValueType::Object:
            if (!std::holds_alternative<std::shared_ptr<variable::Object>>(value.variant()))
                str = u"<" + inout::toU16(getValueTypeName(value.type())) + u">";
            else {
                str   = u"{";
                const variable::VariableSet_t & record = std::get<std::shared_ptr<variable::Object>>(value.variant())->variables();
                bool                            first = true;

                for(const variable::Variable & n : record) {
                    const variable::Variable & n_origin = n.origin();

                    if (n_origin.type() == variable::ValueType::Function && n.name().substr(0,4) == u"set_" && !need_whole_info)
                        continue;

                    if (n_origin.type() == variable::ValueType::Function && n.name().substr(0,4) != u"get_" && !need_whole_info)
                        continue;

                    if (!first)
                        str += u", ";

                    if (n.name().empty())
                        str += u"\"\":" + toString(n.origin().value(), need_whole_info);
                    else if (n_origin.type() == variable::ValueType::Function && n.name().substr(0,4) == u"get_")
                        str += n.name().substr(4) + u":" + toString(n.origin().value(), need_whole_info);
                    else
                        str += n.name() + u":" + toString(n.origin().value(), need_whole_info, true);

                    first = false;
                }
                str += u"}";
            }
            break;
        case variable::ValueType::Array:
            if (!std::holds_alternative<std::shared_ptr<variable::Array>>(value.variant()))
                str = u"<"+inout::toU16(variable::getValueTypeName(value.type()))+ u">";
            else {
                str = u"[";
                const std::shared_ptr<variable::Array> array = value.getArray();
                bool first = true;

                for(size_t i=0; i < array->values().size(); ++i)
                {
                    if (!first) str += u", ";
                    str += toString(array->values()[i], need_whole_info, true);
                    first = false;
                }
                str += u"]";
            }
            break;
        default:
            {
                str = u"<" + inout::toU16(getValueTypeName(value.type()));

                if ((value.type() == variable::ValueType::IntFunction && std::holds_alternative<variable::InternalFunction>(value.variant()))
                || (value.type() == variable::ValueType::ExtFunction && std::holds_alternative<variable::ExternalFunction>(value.variant())))
                    str += u"{}"; // Обозначаем непустую (определённую) функцию

                str+=u">";
            }
            break;
        }

        // if (need_whole_info && !origin.spec().variables().empty())
        //     str += u"#" + toString({{}, origin.spec(), {}, {}}, need_whole_info);

        return str;
    }

}